/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2018-2019.
 * Description: support led
 * Author: qiuxi <qiuxi1@huawei.com>
 * Create: 2018-11-04
 *
 * Dec 2019 - zhangwen <zhangwen8@huawei.com>
 * refactor led
 */
#include <linux/types.h>
#include <linux/uaccess.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/io.h>
#include <linux/bio.h>
#include <linux/module.h>
#include <linux/ioctl.h>
#include <linux/miscdevice.h>

#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_fdt.h>
#include <linux/of_irq.h>
#include <linux/of_platform.h>
#include <linux/hal/gpio_lock.h>
#include <linux/hal/cpu_param.h>
#include <linux/timer.h>
#include <linux/hal/hal_log.h>
#include <linux/hal/led.h>
#include <linux/hal/common.h>
#include <linux/acpi.h>
#include <linux/hal/acpi_op.h>
#include <linux/rtos_acpi_id.h>


#include "../gpio.h"

#include "led_common.h"

#if defined(CONFIG_ARM)  || defined(CONFIG_ARM64)
#define  LED_GPIO_DIR_REG    HI_CPU_GPIO_DIR_REG
#define  LED_GPIO_OUTPUT_REG  HI_CPU_GPIO_OUTPUT_REG
#elif defined(CONFIG_PPC32)
#include <../sysdev/fsl_soc.h>
#define  LED_GPIO_DIR_REG  0x00
#define  LED_GPIO_OUTPUT_REG  0x08
#else
#error "the arch is not supported yet"
#endif


#define LED_FREQ_DEFAULT 2
#define LED_FREQ_MAX 10
#define LED_PERIOD_DEFAULT 1000
#define LED_PERIOD_MIN (1000 / HZ)
#define LED_PERIOD_MAX 10000

#define LOW_LEVEL 0
#define HIGH_LEVEL 1
#define OUTPUT_PIN 1

static unsigned int led_period;
#define LED_PERIOD ((led_period * HZ) / 1000) /* in 1ms */

static unsigned long led_port;
static unsigned int led_port_phy;
static unsigned int led_pin;
static unsigned long red_led_port;
static unsigned int red_led_port_phy;
static unsigned int red_led_pin;
static unsigned int red_off_level = HIGH_LEVEL;

static bool is_led_init;
static struct timer_list led_timer;
static int led_status;

/*
 * add led support for cpld device
 */
static unsigned long cpld_led_base;
static unsigned int cpld_led_base_phy;
static unsigned int cpld_led_offset;
static unsigned int cpld_led_ctrl_bits;
static unsigned int cpld_led_on;
static unsigned int cpld_led_off;

static int port_init(unsigned long *p_reg_base, unsigned long mask,
		unsigned long dir, unsigned long init_val)
{
	unsigned long reg_base = *p_reg_base;
	u32 ret;

#if defined(CONFIG_ARM) || defined(CONFIG_ARM64)
	reg_base = (unsigned long)ioremap(reg_base, 0x1000);
	if (reg_base == 0) {
		pr_err("LED ioremap gpio failed\n");
		return -1;
	}

	rtos_gpio_lock();
	ret = readl((const volatile void *)(reg_base + HI_CPU_GPIO_INT_EN_REG));
	ret &= ~mask;
	writel(ret, (volatile void *)(reg_base + HI_CPU_GPIO_INT_EN_REG));
	/* set as output pin */
	ret = readl((volatile void *)(reg_base + HI_CPU_GPIO_DIR_REG));
	if (likely(cpu_type_num != SD8081_CPU))
		ret |= mask;
	else
		ret &= ~mask;
	writel(ret, (volatile void *)(reg_base + HI_CPU_GPIO_DIR_REG));
	rtos_gpio_unlock();
#elif defined(CONFIG_PPC32)
	reg_base = (unsigned long)ioremap(get_immrbase() + reg_base, 0x1000);
	if (reg_base == 0) {
		pr_err("LED ioremap gpio failed\n");
		return -1;
	}

	/* set as output pin */
	rtos_gpio_lock();
	ret = readl((volatile void *)(reg_base + LED_GPIO_DIR_REG));
	ret |= mask;
	writel(ret, (volatile void *)(reg_base + LED_GPIO_DIR_REG));
	rtos_gpio_unlock();

#endif

	*p_reg_base = reg_base;

	return 0;
}

static void set_port_level(unsigned long reg_base, unsigned long mask, unsigned long val)
{
	unsigned int ret;

	rtos_gpio_lock();
	ret = readl((const volatile void *)(reg_base + LED_GPIO_OUTPUT_REG));
	if (val)
		ret |= mask;
	else
		ret &= ~mask;
	writel(ret, (volatile void *)(reg_base + LED_GPIO_OUTPUT_REG));
	rtos_gpio_unlock();
}

static u32 get_port_level_inner(unsigned long reg_base, unsigned long mask)
{
	u32 ret;

	rtos_gpio_lock();
	ret = ((u32)readl((const volatile void *)(reg_base + LED_GPIO_OUTPUT_REG))) & mask;
	rtos_gpio_unlock();

	return ret;
}

static u16 get_cpld_value(unsigned long reg_base)
{
	u16 ret;

	ret = ((u16)readw((const volatile void *)(reg_base + cpld_led_offset))) & cpld_led_ctrl_bits;

	return ret;
}

static int cpld_led_init(unsigned long *p_reg_base)
{
	unsigned long reg_base = *p_reg_base;

	reg_base = (unsigned long)ioremap(reg_base, 0x1000);
	if (reg_base == 0) {
		pr_err("LED ioremap cpld failed\n");
		return -1;
	}

	*p_reg_base = reg_base;

	return 0;
}

static void set_cpld_value(unsigned long reg_base, unsigned int mask)
{
	unsigned int ret;

	ret = readw((const volatile void *)(reg_base + cpld_led_offset));
	ret = (~cpld_led_ctrl_bits & ret) | (cpld_led_ctrl_bits & mask);
	writew(ret, (volatile void *)(reg_base + cpld_led_offset));
}

static int bsp_led_info_get(void)
{
	struct device_node *node = NULL;
	unsigned int read;
	unsigned int iomg_set, iomg_clear;
	unsigned int iomg_base;
	unsigned int led_freq = LED_FREQ_DEFAULT;
	void __iomem *iomg_base_addr = NULL;

	node = of_find_compatible_node(NULL, NULL, "rtos_led");
	if (node == NULL)
		return -ENODEV;
	if (of_property_read_u32(node, "freq", &led_freq)
		&& of_property_read_u32(node, "period", &led_period))
		return -EINVAL;

	if (led_freq > LED_FREQ_MAX || led_freq == 0) {
		pr_err("LED freq is not valid\n");
		led_freq = LED_FREQ_DEFAULT;
	}

	/* if led_period not set, we use led_freq in stead */
	if (led_period == 0)
		led_period = LED_PERIOD_DEFAULT / led_freq;

	if (led_period > LED_PERIOD_MAX)
		led_period = LED_PERIOD_MAX;
	else if (led_period < LED_PERIOD_MIN)
		led_period = LED_PERIOD_MIN;

	/* read the node of cpld_led */
	node = of_find_compatible_node(NULL, NULL, "rtos_led_cpld_fixpart");
	if (node == NULL)
		goto gpio;
	else
		led_type = LED_CPLD;

	if (of_property_read_u32(node, "base", &cpld_led_base_phy)) {
		pr_err("LED cpld base failed\n");
		return -EINVAL;
	}
	cpld_led_base = (unsigned long)cpld_led_base_phy;

	if (of_property_read_u32(node, "offset", &cpld_led_offset)) {
		pr_err("LED cpld offset failed\n");
		return -EINVAL;
	}

	if (of_property_read_u32(node, "cpld_ctrl_bits", &cpld_led_ctrl_bits)) {
		pr_err("LED cpld cpld_ctrl_bits failed\n");
		return -EINVAL;
	}

	if (of_property_read_u32(node, "led_on_state", &cpld_led_on)) {
		pr_err("LED cpld led_on_state failed\n");
		return -EINVAL;
	}

	if (of_property_read_u32(node, "led_off_state", &cpld_led_off)) {
		pr_err("LED cpld led_off_state failed\n");
		return -EINVAL;
	}

	/* only support one kind of led controller,so return here */
	return 0;

gpio:
	node = of_find_compatible_node(NULL, NULL, "rtos_led_fixpart");
	if (node == NULL) {
		pr_err("LED device not exist\n");
		return -ENODEV;
	} else {
		led_type = LED_GPIO;
	}

	if (of_property_read_u32(node, "port", &led_port_phy)) {
		pr_err("LED gpio port failed\n");
		return -EINVAL;
	}
	led_port = (unsigned long)led_port_phy;

	if (of_property_read_u32(node, "pin", &led_pin)) {
		pr_err("LED gpio pin failed\n");
		return -EINVAL;
	}

	if (of_property_read_u32(node, "red_port", &red_led_port_phy))
		red_led_port = led_port;
	else
		red_led_port = (unsigned long)red_led_port_phy;

	if (of_property_read_u32(node, "red_pin", &red_led_pin)) {
		pr_err("LED red_pin failed\n");
		return -EINVAL;
	}

	if (of_property_read_u32(node, "red_level", &red_off_level))
		red_off_level = HIGH_LEVEL;

	if (of_property_read_u32(node, "iomg_base", &iomg_base))
		return 0;
	if (of_property_read_u32(node, "iomg_set", &iomg_set))
		iomg_set = 0;
	if (of_property_read_u32(node, "iomg_clear", &iomg_clear))
		iomg_clear = 0;
	/* if the io is reused, we need to set it as gpio */
	iomg_base_addr = ioremap((unsigned long)iomg_base, 0x4);
	if (!iomg_base_addr) {
		pr_err("LED ioremap iomg_base failed\n");
		return -ENOMEM;
	}

	read = readl(iomg_base_addr);
	read |= iomg_set;
	read &= ~iomg_clear;
	/* set the io as gpio */
	writel(read, iomg_base_addr);
	iounmap(iomg_base_addr);
	pr_warn("we set the io of led as gpio mode\n");

	return 0;
}

static int led_info_get(void)
{
	struct device_node *np = NULL;
	unsigned int led_freq = LED_FREQ_DEFAULT;

	np = of_find_compatible_node(NULL, NULL, "rtos_led");
	if (!np) {
		pr_warn("can't find led node, please check it\n");
		return -ENODEV;
	}

	of_property_read_u32(np, "freq", &led_freq);
	if ((led_freq > LED_FREQ_MAX) || (led_freq == 0)) {
		pr_err("led freq is invalid, use default value\n");
		led_freq = LED_FREQ_DEFAULT;
	}

	of_property_read_u32(np, "period", &led_period);
	if (led_period == 0) /* if led_period not set, we use led_freq instead */
		led_period = LED_PERIOD_DEFAULT / led_freq;

	if (led_period > LED_PERIOD_MAX)
		led_period = LED_PERIOD_MAX;
	else if (led_period < LED_PERIOD_MIN)
		led_period = LED_PERIOD_MIN;

	np = of_find_compatible_node(NULL, NULL, "rtos_led_ctrl");
	if (!np) {
		pr_warn("can't find led ctrl node, please check it\n");
		return -ENODEV;
	}

	led_map(DROL_IOREMAP, DROL_IOUNMAP);

	return 0;
}

static void led_flick(enum led_mode_t mode)
{
	if (led_type == LED_GPIO)
		rtos_gpio_lock();

	drol_do_op(drol_led_match[mode].op, drol_led_match[mode].op_nr);
	led_status = mode;

	if (led_type == LED_GPIO)
		rtos_gpio_unlock();
}

static void bsp_led_timer_handler(struct timer_list *timer)
{
	static unsigned int green_on = 1;

	if (led_type == LED_GPIO) {
		/* green led flash */
		if (green_on) {
			set_port_level(led_port, led_pin, LOW_LEVEL);
			green_on = 0;
		} else {
			set_port_level(led_port, led_pin, HIGH_LEVEL);
			green_on = 1;
		}
	} else if (led_type == LED_CPLD) {
		if (green_on) {
			set_cpld_value(cpld_led_base, cpld_led_off);
			green_on = 0;
		} else {
			set_cpld_value(cpld_led_base, cpld_led_on);
			green_on = 1;
		}
	} else {
		pr_warn("NO Led device!\n");
	}

	/* modify timer for the next trigger */
	 mod_timer(timer, jiffies + LED_PERIOD);
}

static void led_timer_handler(struct timer_list *timer)
{
	static unsigned int green_on = 1;

	if (green_on) {
		led_flick(LED_OFF);
		green_on = 0;
	} else {
		led_flick(LED_ON);
		green_on = 1;
	}

	mod_timer(timer, jiffies + LED_PERIOD);
}

static void led_timer_stop(void)
{
	int ret;

	ret = del_timer_sync(&led_timer);
	if (ret)
		pr_info("stop LED timer\n");
}

static void led_timer_start(void)
{
	if (is_led_use_new_arch())
		timer_setup(&led_timer, led_timer_handler, 0);
	else
		timer_setup(&led_timer, bsp_led_timer_handler, 0);
	led_timer.expires = jiffies + LED_PERIOD;
	add_timer(&led_timer);
}

static void green_led_off(void)
{
	if (led_type == LED_GPIO)
		set_port_level(led_port, led_pin, HIGH_LEVEL);
	else if (led_type == LED_CPLD)
		set_cpld_value(cpld_led_base, cpld_led_off);
}

static void green_led_on(void)
{
	if (led_type == LED_GPIO)
		set_port_level(led_port, led_pin, LOW_LEVEL);
	else if (led_type == LED_CPLD)
		set_cpld_value(cpld_led_base, cpld_led_on);
}

static void green_led_reverse(void)
{
	unsigned int pin_level;

	/*
	 * why not GetPortLevel? because we want to get the led inner level,
	 * but GetPortLevel return led outer level, it is always 1.
	 */
	if (led_type == LED_GPIO) {
		pin_level = get_port_level_inner(led_port, led_pin);
		if (pin_level)
			pin_level = 0;
		else
			pin_level = 1;
		set_port_level(led_port, led_pin, pin_level);
	} else if (led_type == LED_CPLD) {
		if (get_cpld_value(cpld_led_base) == cpld_led_on)
			set_cpld_value(cpld_led_base, cpld_led_off);
		else
			set_cpld_value(cpld_led_base, cpld_led_on);
	}
}

static long bsp_led_ioctl(struct file *file,
		unsigned int cmd, unsigned long arg)
{
	int err = 0;
	static bool first_cmd = true;
	led_t *arg_local = (led_t *)(uintptr_t)arg;
	led_t ledset = 0;

	/* add check for file */
	if (led_type == LED_NO_DEVICE)
		return -ENXIO;

	if (true == first_cmd) {
		if (led_type == LED_GPIO) {
			set_port_level(red_led_port, red_led_pin, red_off_level); /* close red led */
		} else if (led_type == LED_CPLD) {
			set_cpld_value(cpld_led_base, cpld_led_off);
		}

		led_timer_stop();
		first_cmd = false;
	}

	if (copy_from_user((void *)&ledset, (void __user *)arg_local, sizeof(led_t)))
		return -EFAULT;

	switch (ledset) {
	case PROG_GREEN_LED_OFF:
		green_led_off();
		break;
	case PROG_GREEN_LED_ON:
		green_led_on();
		break;
	case PROG_GREEN_LED_RERERVSE:
		green_led_reverse();
		break;
	default:
		err = -EINVAL;
		break;
	}

	return err;
}

long rtos_led_control(led_t ledset)
{
	static bool first_cmd = true;

	if (!is_led_init)
		return -ENXIO;

	if (true == first_cmd) {
		led_flick(LED_OFF);
		led_timer_stop();
		first_cmd = false;
	}

	switch (ledset) {
	case PROG_GREEN_LED_OFF:
		led_flick(LED_OFF);
		break;
	case PROG_GREEN_LED_ON:
		led_flick(LED_ON);
		break;
	case PROG_GREEN_LED_RERERVSE:
		if (led_status == LED_ON)
			led_flick(LED_OFF);
		else if (led_status == LED_OFF)
			led_flick(LED_ON);
		break;
	default:
		return -EINVAL;
		break;
	}

	return 0;
}
EXPORT_SYMBOL_NS(rtos_led_control, HW_RTOS_NS);

static long led_ioctl(struct file *file,
		unsigned int cmd, unsigned long arg)
{
	led_t *arg_local = (led_t *)(uintptr_t)arg;
	led_t ledset = 0;

	if (copy_from_user((void *)&ledset, (void __user *)arg_local, sizeof(led_t)))
		return -EFAULT;

	return rtos_led_control(ledset);
}

#ifdef CONFIG_COMPAT
static long bsp_led_compat_ioctl(struct file *file, unsigned int cmd,
				  unsigned long arg)
{
	unsigned long translated_arg = (unsigned long)(uintptr_t)compat_ptr(arg);

	return bsp_led_ioctl(file, cmd, translated_arg);
}

static long led_compat_ioctl(struct file *file, unsigned int cmd,
				  unsigned long arg)
{
	unsigned long translated_arg = (unsigned long)(uintptr_t)compat_ptr(arg);

	return led_ioctl(file, cmd, translated_arg);
}
#endif

static struct file_operations led_fops = {
	.owner   = THIS_MODULE,
	.unlocked_ioctl = bsp_led_ioctl,
#ifdef CONFIG_COMPAT
	.compat_ioctl   = bsp_led_compat_ioctl,
#endif
};

static struct miscdevice led_misc_dev = {
	.minor	= MISC_DYNAMIC_MINOR,	/* get the minor number dynmicly */
	.name   = "led",
	.fops	= &led_fops,
};

static void bsp_led_init(void)
{
	if (bsp_led_info_get()) {
		led_type = LED_NO_DEVICE;
		pr_warn("Cannot find led info in dtb,make sure there is no led on board\n");
		return;
	}

	if (led_type == LED_GPIO) {
		port_init(&led_port, led_pin, OUTPUT_PIN, 1);
		port_init(&red_led_port, red_led_pin, OUTPUT_PIN, 1);
		set_port_level(red_led_port, red_led_pin, red_off_level);
	} else if (led_type == LED_CPLD) {
		cpld_led_init(&cpld_led_base);
		set_cpld_value(cpld_led_base, cpld_led_off);
	} else {
		pr_warn("NO Led device!\n");
	}
	led_timer_start();

	is_led_init = true;
}

static void led_init(void)
{
	if (led_info_get()) {
		led_type = LED_NO_DEVICE;
		pr_info("Can't find led info in dtb,make sure there is no led on board\n");
		return;
	}

	led_timer_start();

	is_led_init = true;
}

void __init led_breathing(void)
{
	if (is_led_use_new_arch())
		led_init();
	else
		bsp_led_init();
}

#ifdef CONFIG_ACPI
static acpi_status led_parse_device(acpi_handle handle, u32 level,
	void *context, void **retval)
{
	unsigned int led_freq = LED_FREQ_DEFAULT;
	struct acpi_device device;

	acpi_get_acpi_device(handle, &device, RTOS_ACPI_HID_LED);

	acpi_getprop_u32(&led_freq, &device, "freq");
	if ((led_freq > LED_FREQ_MAX) || (led_freq == 0)) {
		pr_err("led freq is invalid, use default value\n");
		led_freq = LED_FREQ_DEFAULT;
	}

	acpi_getprop_u32(&led_period, &device, "period");
	if (led_period == 0) /* if led_period not set, we use led_freq instead */
		led_period = LED_PERIOD_DEFAULT / led_freq;

	if (led_period > LED_PERIOD_MAX)
		led_period = LED_PERIOD_MAX;
	else if (led_period < LED_PERIOD_MIN)
		led_period = LED_PERIOD_MIN;

	led_acpi_base_init(&device, DROL_IOREMAP, DROL_IOUNMAP);
	return AE_OK;
}

void __init led_acpi_breathing(void)
{
	static acpi_status status;

	if (acpi_disabled)
		return;

	status = acpi_get_devices(RTOS_ACPI_HID_LED, led_parse_device, NULL, NULL);
	if (ACPI_FAILURE(status)) {
		led_type = LED_NO_DEVICE;
		pr_info("Can't find led info in acpi,make sure there is no led on board\n");
		return;
	}

	led_timer_start();
	is_led_init = true;
}
#endif

static void __exit led_misc_exit(void)
{
	led_timer_stop();
	misc_deregister(&led_misc_dev);
}

static int __init led_misc_init(void)
{
	int ret = 0;

	if (!is_led_init)
		return ret;

	if (is_led_use_new_arch()) {
		led_fops.unlocked_ioctl = led_ioctl;
#ifdef CONFIG_COMPAT
		led_fops.compat_ioctl = led_compat_ioctl;
#endif
	} else {
		led_fops.unlocked_ioctl = bsp_led_ioctl;
#ifdef CONFIG_COMPAT
		led_fops.compat_ioctl = bsp_led_compat_ioctl;
#endif
	}

	ret = misc_register(&led_misc_dev);
	if (ret)
		pr_err("LED register failed with ret %d.\n", ret);
	else
		pr_info("LED registed.\n");

	return ret;
}

module_init(led_misc_init);
module_exit(led_misc_exit);
MODULE_DESCRIPTION("hisi led");
MODULE_LICENSE("GPL");
MODULE_AUTHOR("HUAWEI TECHNOLOGIES CO., LTD.");

